Skip to content

Added Circuit Breaker Module#5499

Closed
Subhanshu20135 wants to merge 5 commits intoktorio:mainfrom
The-Developer-Diaries:feature/circuit-breaker
Closed

Added Circuit Breaker Module#5499
Subhanshu20135 wants to merge 5 commits intoktorio:mainfrom
The-Developer-Diaries:feature/circuit-breaker

Conversation

@Subhanshu20135
Copy link
Copy Markdown

PR: Add Circuit Breaker client plugin

Summary

  • Adds a new ktor-client-circuit-breaker module implementing the Circuit Breaker pattern for the Ktor HTTP client, filling the gap alongside HttpRequestRetry and HttpTimeout for microservice resilience.
  • Implements a three-state machine (Closed / Open / Half-Open) with per-service configuration, automatic request routing, custom failure predicates, and thread-safe state transitions via coroutine Mutex.
  • Includes 14 test cases covering all state transitions, recovery cycles, independent circuits, custom predicates, request routing, configuration validation, and exception semantics.

Motivation

The Ktor client has retry and timeout support but no built-in circuit breaker. Without one, a single failing downstream service can exhaust connection pools and cascade failures across the call graph. This is a table-stakes pattern for microservice architectures and is present in competing frameworks (Spring Cloud Circuit Breaker, Resilience4j, Hystrix).

API

val client = HttpClient(CIO) {
    install(CircuitBreaker) {
        register("payment-service") {
            failureThreshold = 5
            resetTimeout = 30.seconds
            halfOpenRequests = 3
        }
    }
}

client.get("https://payment.example.com/api/charge") {
    circuitBreaker("payment-service")
}

Design decisions

  • on(Send) hook -- Same interception point as HttpRequestRetry, giving the circuit breaker visibility into the final outcome after any inner retry logic.
  • IllegalStateException -- CircuitBreakerOpenException extends IllegalStateException rather than IOException because a circuit breaker rejection is a deliberate policy decision, not an I/O error. This also avoids unintended retries by HttpRequestRetry's default exception predicate.
  • TimeSource abstraction -- An internal timeSource property defaults to TimeSource.Monotonic but can be replaced with TestTimeSource in tests, enabling deterministic time-based state transition tests without real delays.
  • Coroutine Mutex -- Used for state transitions instead of atomics, since the state machine has multi-field invariants that need to be updated atomically.

Files changed

File Change
settings.gradle.kts Register ktor-client-circuit-breaker module
.../ktor-client-circuit-breaker/build.gradle.kts Module build config
.../circuitbreaker/CircuitBreaker.kt Plugin implementation (~365 lines)
.../circuitbreaker/CircuitBreakerTest.kt 14 test cases
.../api/ktor-client-circuit-breaker.api JVM ABI dump
.../api/ktor-client-circuit-breaker.klib.api Klib ABI dump
.../ktor-client-circuit-breaker/README.md Module documentation

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Ktor HTTP client Circuit Breaker plugin with per-name state machines (CLOSED → OPEN → HALF-OPEN), configurable thresholds, reset timeout, half-open trial limits, request tagging/routing, failure predicate, exception type, README, build files, and tests.

Changes

Cohort / File(s) Summary
Documentation
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md
New README documenting plugin semantics, state transitions, request tagging/routing, failure classification, OPEN rejection, and installation ordering notes.
Public API Surface
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api, .../api/ktor-client-circuit-breaker.klib.api
Introduces CircuitBreakerConfig, ServiceCircuitBreakerConfig, CircuitBreakerOpenException, plugin accessor, and HttpRequestBuilder.circuitBreaker(name) extension; exposes public getters/setters and configuration functions.
Build & Settings
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts, settings.gradle.kts
Adds new client-plugin subproject, build script, and test dependencies; registers ktor-client-circuit-breaker in project settings.
Implementation
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/.../CircuitBreaker.kt
New plugin implementation with per-name CircuitBreakerInstance state machines, acquire/record logic, half-open request limiting, routing and request-attribute resolution, failure predicate handling, and CircuitBreakerOpenException.
Tests
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/.../CircuitBreakerTest.kt
Comprehensive tests covering state transitions, thresholds, reset behavior, half-open trials, multi-name isolation, routing vs explicit tagging, custom failure predicate, exception fields, and invalid configuration validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • bjhham
  • e5l
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Added Circuit Breaker Module' clearly and concisely summarizes the main change—introduction of a new circuit breaker plugin module for the Ktor HTTP client.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering motivation, solution, design decisions, API usage, and a file change summary, exceeding the basic template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 173-176: The HALF_OPEN branch currently throws
CircuitBreakerOpenException when halfOpenAttempts >= config.halfOpenRequests
which misleadingly reports the circuit as OPEN; update this to either throw a
distinct exception (e.g., CircuitBreakerHalfOpenSaturatedException) or at
minimum change the error message to indicate the circuit is HALF_OPEN and
saturated (include name, config.resetTimeout and current
halfOpenAttempts/config.halfOpenRequests), and adjust any callers that rely on
CircuitBreakerOpenException to handle the new exception or message accordingly;
locate this logic in the block that checks CircuitState.HALF_OPEN and references
halfOpenAttempts and config.halfOpenRequests to make the change.
- Around line 351-355: The catch-all in CircuitBreaker.kt around the
proceed(request) call records every Throwable via instance.recordFailure(),
which incorrectly treats CancellationException as a service failure; update the
try/catch in the call execution (the block that calls proceed(request) inside
the CircuitBreaker plugin) to explicitly rethrow CancellationException (and
subclasses) without calling instance.recordFailure(), and only call
instance.recordFailure() for non-cancellation throwables; then add a regression
test exercising a locally-cancelled request to assert the circuit state does not
record a failure (i.e., circuit stays closed).
- Around line 329-335: The unsynchronized read of the MutableMap instances in
getInstance creates a race; move the initial lookup inside the
instancesMutex.withLock so the entire lookup-and-create path is protected.
Specifically, remove the pre-lock instances[name]?.let return check and perform
instances.getOrPut(name) { CircuitBreakerInstance(name, config.globalConfig,
timeSource) } inside instancesMutex.withLock in the getInstance function so all
access to instances is guarded by instancesMutex.

In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 13-30: The fenced diagram in the README.md lacks a language tag
which triggers markdownlint MD040; update the fence that contains the
circuit-breaker state diagram so the opening triple-backtick includes the
language identifier text (i.e., change the fence from ``` to ```text) so the
markdown linter recognizes it as a plain text block and the lint error is
resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e5c17e8-4d22-40e4-b796-0e667c5fec4a

📥 Commits

Reviewing files that changed from the base of the PR and between 1567ffe and 54fb87c.

📒 Files selected for processing (7)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
  • settings.gradle.kts

@Subhanshu20135 Subhanshu20135 force-pushed the feature/circuit-breaker branch from 54fb87c to 552b182 Compare March 30, 2026 19:02
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md (1)

13-30: ⚠️ Potential issue | 🟡 Minor

Still missing a language on the diagram fence.

Line 13 still opens the state-diagram block without a language, so markdownlint MD040 will keep firing.

✏️ Suggested fix
-```
+```text
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md` around
lines 13 - 30, The code block containing the ASCII state diagram in README.md is
missing a language specifier; update the opening fence (the triple backticks
that start the ASCII diagram block) to include a language such as text (i.e.,
change ``` to ```text) so markdownlint MD040 stops flagging it and the diagram
renders correctly.
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (3)

329-335: ⚠️ Potential issue | 🔴 Critical

Still reading instances outside instancesMutex.

Line 330 performs an unsynchronized read from a shared MutableMap before acquiring instancesMutex. Since dynamic circuit creation writes to the same map inside the lock, this is still a real race; please move the whole lookup into the critical section.

🔒 Suggested fix
     suspend fun getInstance(name: String): CircuitBreakerInstance {
-        instances[name]?.let { return it }
         return instancesMutex.withLock {
             instances.getOrPut(name) {
                 CircuitBreakerInstance(name, config.globalConfig, timeSource)
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 329 - 335, The current getInstance reads the shared MutableMap
instances outside the instancesMutex, causing a race; move the entire
lookup/creation logic into the critical section by removing the early
unsynchronized instances[name] check and performing instances.getOrPut(name)
inside instancesMutex.withLock so that all accesses to instances (read or write)
happen under instancesMutex; keep creating the CircuitBreakerInstance(name,
config.globalConfig, timeSource) as the getOrPut default.

351-355: ⚠️ Potential issue | 🟠 Major

Still counting cancellations as downstream failures.

Line 353 catches every Throwable, and Line 354 records it as a circuit failure. That still includes coroutine cancellation, so caller-aborted requests can open the breaker even when the service is healthy. Re-throw CancellationException before recordFailure(), add a regression test for a locally cancelled request, and tighten the README wording so it does not imply that all exceptions count.

🛠️ Suggested fix
         val call = try {
             proceed(request)
         } catch (cause: Throwable) {
+            if (cause is CancellationException) throw cause
             instance.recordFailure()
             throw cause
         }

This checks the current catch block, confirms how CancellationException is defined in the repo, and shows whether this plugin already has cancellation-focused coverage. Expect the catch block to lack a cancellation guard and the test search to return no dedicated regression case.

#!/bin/bash
set -euo pipefail

FILE='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt'
TEST_FILE='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt'

echo "== catch block in CircuitBreaker plugin =="
sed -n '349,356p' "$FILE"

echo
echo "== repo definitions of CancellationException =="
rg -n --type=kt 'typealias\s+CancellationException\s*=|class\s+CancellationException\b'

echo
echo "== cancellation-related coverage in CircuitBreakerTest =="
rg -n --type=kt 'CancellationException|cancel' "$TEST_FILE"

Based on learnings, io.ktor.utils.io.CancellationException is a typealias for kotlinx.coroutines.CancellationException, so that single type check excludes all coroutine cancellation exceptions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 351 - 355, The catch in the CircuitBreaker around proceed(request)
currently treats every Throwable as a failure; change the handler so that
CancellationException is re-thrown immediately (e.g., in the catch for cause:
Throwable do if (cause is CancellationException) throw cause) before calling
instance.recordFailure() and re-throwing other exceptions; add a regression test
in CircuitBreakerTest that cancels a locally started request and asserts the
breaker does not record/open from that cancellation; and update the README
wording for the circuit breaker to explicitly state that coroutine cancellations
are excluded from failure counts.

173-176: ⚠️ Potential issue | 🟡 Minor

Still reporting HALF_OPEN saturation as OPEN.

Line 175 rejects extra half-open probes with CircuitBreakerOpenException, but this branch is saturated HALF_OPEN, not OPEN. That makes the exposed state and retry timing wrong for callers, because another request may be allowed as soon as a probe finishes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 173 - 176, The code currently throws CircuitBreakerOpenException
when halfOpenAttempts >= config.halfOpenRequests inside the
CircuitState.HALF_OPEN branch; change this to surface the correct HALF_OPEN
condition: replace the thrown CircuitBreakerOpenException(name,
config.resetTimeout) with a distinct exception (e.g.,
CircuitBreakerHalfOpenException) that indicates the circuit is saturated in
HALF_OPEN (or extend CircuitBreakerOpenException to accept a state parameter and
pass CircuitState.HALF_OPEN). Update any callers/tests to expect the new
exception type or check the returned state accordingly; reference
CircuitState.HALF_OPEN, halfOpenAttempts, config.halfOpenRequests, and the
thrown exception in your changes.
🧹 Nitpick comments (1)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (1)

267-277: Add the standard feedback link to this public extension KDoc.

HttpRequestBuilder.circuitBreaker is public API, but its KDoc stops after the description. Please add the repo’s usual [Report a problem] footer so the generated docs stay consistent with other public extensions. Based on learnings, public API extension functions in this repo should include a brief description and a [Report a problem] link.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 267 - 277, The public extension function
HttpRequestBuilder.circuitBreaker is missing the repo's standard "[Report a
problem]" KDoc footer; update the KDoc for circuitBreaker (the block above the
function that references CircuitBreakerConfig.register and
CircuitBreakerConfig.global) to append the standard Report a problem link/footer
used across public APIs so generated docs match other extensions, keeping
existing description and references to CircuitBreakerNameKey unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 329-335: The current getInstance reads the shared MutableMap
instances outside the instancesMutex, causing a race; move the entire
lookup/creation logic into the critical section by removing the early
unsynchronized instances[name] check and performing instances.getOrPut(name)
inside instancesMutex.withLock so that all accesses to instances (read or write)
happen under instancesMutex; keep creating the CircuitBreakerInstance(name,
config.globalConfig, timeSource) as the getOrPut default.
- Around line 351-355: The catch in the CircuitBreaker around proceed(request)
currently treats every Throwable as a failure; change the handler so that
CancellationException is re-thrown immediately (e.g., in the catch for cause:
Throwable do if (cause is CancellationException) throw cause) before calling
instance.recordFailure() and re-throwing other exceptions; add a regression test
in CircuitBreakerTest that cancels a locally started request and asserts the
breaker does not record/open from that cancellation; and update the README
wording for the circuit breaker to explicitly state that coroutine cancellations
are excluded from failure counts.
- Around line 173-176: The code currently throws CircuitBreakerOpenException
when halfOpenAttempts >= config.halfOpenRequests inside the
CircuitState.HALF_OPEN branch; change this to surface the correct HALF_OPEN
condition: replace the thrown CircuitBreakerOpenException(name,
config.resetTimeout) with a distinct exception (e.g.,
CircuitBreakerHalfOpenException) that indicates the circuit is saturated in
HALF_OPEN (or extend CircuitBreakerOpenException to accept a state parameter and
pass CircuitState.HALF_OPEN). Update any callers/tests to expect the new
exception type or check the returned state accordingly; reference
CircuitState.HALF_OPEN, halfOpenAttempts, config.halfOpenRequests, and the
thrown exception in your changes.

In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 13-30: The code block containing the ASCII state diagram in
README.md is missing a language specifier; update the opening fence (the triple
backticks that start the ASCII diagram block) to include a language such as text
(i.e., change ``` to ```text) so markdownlint MD040 stops flagging it and the
diagram renders correctly.

---

Nitpick comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 267-277: The public extension function
HttpRequestBuilder.circuitBreaker is missing the repo's standard "[Report a
problem]" KDoc footer; update the KDoc for circuitBreaker (the block above the
function that references CircuitBreakerConfig.register and
CircuitBreakerConfig.global) to append the standard Report a problem link/footer
used across public APIs so generated docs match other extensions, keeping
existing description and references to CircuitBreakerNameKey unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19839744-739a-4dc8-bcd2-688a4f66c336

📥 Commits

Reviewing files that changed from the base of the PR and between 54fb87c and 552b182.

📒 Files selected for processing (7)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
  • settings.gradle.kts
✅ Files skipped from review due to trivial changes (3)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
🚧 Files skipped from review as they are similar to previous changes (2)
  • settings.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt

@osipxd osipxd self-assigned this Apr 1, 2026
@Subhanshu20135 Subhanshu20135 force-pushed the feature/circuit-breaker branch from 552b182 to 71282ee Compare April 5, 2026 11:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Around line 145-146: The README's plugin order is reversed: change the
guidance so that CircuitBreaker is installed before HttpRequestRetry (i.e.,
install `CircuitBreaker` before `HttpRequestRetry`) so the circuit breaker wraps
the retry logic and observes the final outcome after all retries; keep the
`HttpTimeout` note as-is (timeout exceptions count as circuit breaker failures).
Ensure the text references the exact symbols `CircuitBreaker`,
`HttpRequestRetry`, and `HttpTimeout`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2c1d084-3e78-4af9-b061-4d26765b42ce

📥 Commits

Reviewing files that changed from the base of the PR and between 552b182 and 71282ee.

📒 Files selected for processing (7)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
  • settings.gradle.kts
✅ Files skipped from review due to trivial changes (3)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/build.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.klib.api
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • settings.gradle.kts
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/api/ktor-client-circuit-breaker.api

Comment thread ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md Outdated
Subhanshu20135 and others added 2 commits April 6, 2026 01:43
…mmon/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ADME.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md (1)

13-13: ⚠️ Potential issue | 🟡 Minor

Add text to the ASCII-diagram fence.

The opening fence at Line 13 still trips markdownlint MD040 without a language tag.

✏️ Proposed fix
-```
+```text
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md` at
line 13, The markdown code fence in the ASCII diagram uses a bare
triple-backtick opening fence which triggers markdownlint MD040; update the
opening fence to include a language tag by replacing the opening "```" with
"```text" so the ASCII-diagram block has a language specified (e.g., change the
fence at the ASCII-diagram opening to "```text").
ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt (2)

173-176: ⚠️ Potential issue | 🟡 Minor

Use a HALF_OPEN-specific rejection here.

Line 175 is rejecting because the half-open probe budget is full, not because the circuit is OPEN until resetTimeout elapses. Reusing CircuitBreakerOpenException makes the public exception, message, and KDoc inaccurate for callers. A distinct rejection path, or at least a HALF_OPEN-specific message/KDoc, would avoid that.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 173 - 176, The current HALF_OPEN branch throws
CircuitBreakerOpenException which misrepresents why a request is rejected;
change this to a distinct HALF_OPEN-specific rejection (e.g., introduce and
throw CircuitBreakerHalfOpenException or similar) so the public exception,
message and KDoc reflect that the half-open probe budget (halfOpenAttempts >=
config.halfOpenRequests) is exhausted rather than the circuit being open until
config.resetTimeout; update the thrown type, its message to mention half-open
probe limit and adjust any KDoc/comments referencing CircuitState.HALF_OPEN and
resetTimeout accordingly.

351-355: ⚠️ Potential issue | 🟠 Major

Don’t count coroutine cancellations as downstream failures.

catch (Throwable) currently calls recordFailure() for caller/scope cancellations too, so a locally aborted request can open the circuit even when the service is healthy. Re-throw CancellationException before the catch-all and add a regression test for it; the README note that exceptions “always” count as failures will need the same carve-out.

🛠️ Proposed fix
         val call = try {
             proceed(request)
+        } catch (cause: CancellationException) {
+            throw cause
         } catch (cause: Throwable) {
             instance.recordFailure()
             throw cause
         }
🔎 Verification script
#!/bin/bash
file='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt'
test_file='ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/test/io/ktor/client/plugins/circuitbreaker/CircuitBreakerTest.kt'

sed -n '344,356p' "$file"
printf '\n-- cancellation-related matches --\n'
rg -n -C2 'CancellationException|catch \(cause: Throwable\)|recordFailure\(|cancel' "$file" "$test_file"

Based on learnings, "In Ktor, io.ktor.utils.io.CancellationException is a typealias for kotlinx.coroutines.CancellationException, which is a supertype of JobCancellationException. Therefore, checking e !is io.ktor.utils.io.CancellationException is sufficient to exclude all coroutine cancellation exceptions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`
around lines 351 - 355, The catch-all currently treats every Throwable as a
downstream failure by calling instance.recordFailure() inside the catch (cause:
Throwable) block; change the control flow so CancellationException (use
io.ktor.utils.io.CancellationException /
kotlinx.coroutines.CancellationException) is re-thrown before the generic catch
so cancellations do not call instance.recordFailure(), i.e. add an explicit
check for CancellationException and rethrow it (or add a specific catch for it)
and only call instance.recordFailure() for non-cancellation throwables; update
the tests by adding a regression test in CircuitBreakerTest that simulates a
caller/scope cancellation and asserts the circuit is not opened, and adjust the
README note about exceptions counting as failures to exclude coroutine
cancellations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 329-336: There is a stray closing brace that prematurely ends the
createClientPlugin builder; remove the extra `}` after the getInstance function
so the builder block continues and ensure that resolveCircuitName and the
on(Send) interceptor remain defined inside the createClientPlugin scope (look
for createClientPlugin { ... }, getInstance(...) and then the resolveCircuitName
and on(Send) declarations) so the interceptor code is compiled as part of the
plugin.

---

Duplicate comments:
In
`@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt`:
- Around line 173-176: The current HALF_OPEN branch throws
CircuitBreakerOpenException which misrepresents why a request is rejected;
change this to a distinct HALF_OPEN-specific rejection (e.g., introduce and
throw CircuitBreakerHalfOpenException or similar) so the public exception,
message and KDoc reflect that the half-open probe budget (halfOpenAttempts >=
config.halfOpenRequests) is exhausted rather than the circuit being open until
config.resetTimeout; update the thrown type, its message to mention half-open
probe limit and adjust any KDoc/comments referencing CircuitState.HALF_OPEN and
resetTimeout accordingly.
- Around line 351-355: The catch-all currently treats every Throwable as a
downstream failure by calling instance.recordFailure() inside the catch (cause:
Throwable) block; change the control flow so CancellationException (use
io.ktor.utils.io.CancellationException /
kotlinx.coroutines.CancellationException) is re-thrown before the generic catch
so cancellations do not call instance.recordFailure(), i.e. add an explicit
check for CancellationException and rethrow it (or add a specific catch for it)
and only call instance.recordFailure() for non-cancellation throwables; update
the tests by adding a regression test in CircuitBreakerTest that simulates a
caller/scope cancellation and asserts the circuit is not opened, and adjust the
README note about exceptions counting as failures to exclude coroutine
cancellations.

In `@ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md`:
- Line 13: The markdown code fence in the ASCII diagram uses a bare
triple-backtick opening fence which triggers markdownlint MD040; update the
opening fence to include a language tag by replacing the opening "```" with
"```text" so the ASCII-diagram block has a language specified (e.g., change the
fence at the ASCII-diagram opening to "```text").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65255ade-2e7c-4167-bfe9-c0114371a5b7

📥 Commits

Reviewing files that changed from the base of the PR and between 71282ee and 170cf4c.

📒 Files selected for processing (2)
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/README.md
  • ktor-client/ktor-client-plugins/ktor-client-circuit-breaker/common/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt

Subhanshu20135 and others added 2 commits April 6, 2026 02:31
…mmon/src/io/ktor/client/plugins/circuitbreaker/CircuitBreaker.kt

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@osipxd
Copy link
Copy Markdown
Member

osipxd commented Apr 9, 2026

Thanks for your work on this 🙏
Unfortunately I can't accept the PR. The reasoning is the same as in my comment on the health check PR: #5498 (comment).

@osipxd osipxd closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants